As baseline model model based on tf-idf features was chosen.
Good side of this model: it can be easily seen on what features(=tokens) model's predictions are based.
For explanation of model output line framework will be used.
## for data
import json
import pandas as pd
import numpy as np
## for plotting
import matplotlib.pyplot as plt
import seaborn as sns
## for processing
import re
import nltk
## for bag-of-words
from sklearn import model_selection, naive_bayes
from sklearn import pipeline, manifold, preprocessing, feature_selection, feature_extraction
## for explainer
from lime import lime_text
# for text preprocessing
from pymorphy2 import MorphAnalyzer
from nltk.corpus import stopwords
# custom
from text_classifier.common.preprocessing import preprocess_text
RANDOM_SEED = 123
import nltk
# nltk.download()
df = pd.read_csv("../data/interim/train_no_dup.csv", index_col=0)
The dataset is almost balanced.
stopwords_ru = stopwords.words("russian")
morph = MorphAnalyzer()
df["text_clean"] = df["text"].apply(lambda x: preprocess_text(x, morph, stopwords_ru ))
df.head()
| oid | category | text | text_clean | |
|---|---|---|---|---|
| 0 | 365271984 | winter_sport | Волшебные фото Виктория Поплавская ЕвгенияМедв... | волшебный фото виктория поплавский евгениямедв... |
| 1 | 503385563 | extreme | Возвращение в подземелье Треша 33 Эйфория тупо... | возвращение подземелье треш число эйфория тупо... |
| 2 | 146016084 | football | Лучшие чешские вратари – Доминик Доминатор Гаш... | хороший чешский вратарь доминик доминатор гаше... |
| 3 | 933865449 | boardgames | Rtokenoid Warhammer40k валрак решил нас подкор... | число валрак решить подкормить сильно свежий с... |
| 4 | 713550145 | hockey | Шестеркин затаскивает Рейнджерс в финал Восточ... | шестёркин затаскивать рейнджерс финал восточны... |
df.to_csv("../data/processed/train_tf_idf.csv")
from sklearn.model_selection import GroupShuffleSplit
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=0)
train_index, test_index = next(gss.split(df, df.category, groups=df.oid))
train_index.shape, test_index.shape
((28516,), (7115,))
df_train = df.iloc[train_index]
df_test = df.iloc[test_index]
y_train = df_train["category"].values
y_test = df_test["category"].values
df_train.to_csv("../data/processed/tran_split.csv")
df_test.to_csv("../data/processed/val_split.csv")
len(set(df_test.oid).intersection(df_train.oid))
0
CLASSES = np.unique(y_train)
## Tf-Idf
vectorizer = feature_extraction.text.TfidfVectorizer(max_features=10000, ngram_range=(1,2))
corpus = df_train["text_clean"]
vectorizer.fit(corpus)
X_train = vectorizer.transform(corpus)
dic_vocabulary = vectorizer.vocabulary_
df_for_visualization = X_train.todense()[:,np.random.randint(0, X_train.shape[1],100)]==0
sns.heatmap(df_for_visualization, vmin=0, vmax=1, cbar=False).set_title('Sparse Matrix Sample');
X_train.todense().shape
(28516, 10000)
dic_vocabulary['аут']
456
dic_vocabulary['привет']
6183
y = df_train["category"]
X_names = vectorizer.get_feature_names_out()
p_value_limit = 0.9
selected_features = set()
df_features = []
for cat in np.unique(y):
binary_target = y==cat
chi2, p = feature_selection.chi2(X_train, binary_target)
for feat_name, chi2_val, p_val in zip(X_names, chi2, p):
if 1 - p_val > p_value_limit:
selected_features.add(feat_name)
df_features.append({"y": cat, "feature": feat_name, "score": 1 - p_val})
len(selected_features)
7613
df_features = pd.DataFrame(df_features).sort_values("score")
I reduced the number of features from 10,000 to 7563 by keeping the most statistically relevant ones. Let’s print some:
for cat in np.unique(y):
print("# {}:".format(cat))
print(" . selected features:", len(df_features[df_features["y"]==cat]))
print(" . top features:", ",".join(df_features[df_features["y"]==cat]["feature"].values[:10]))
print(" ")
# athletics: . selected features: 959 . top features: мл,утром,круг турнир,школьный,погода,цена число,фото,мужчина,бывший,первый # autosport: . selected features: 963 . top features: второй круг,поздравлять день,поступать,именно,конец,финал сезон,борьба победа,понимать,завоевать,наспорт # basketball: . selected features: 668 . top features: вес,игрок мир,де,бег спорт,бокс,утро число,упражнение,событие,первый половина,уничтожать # boardgames: . selected features: 1069 . top features: решать,биатлон,ключ,наверняка,подробно,выступление,металлург,женский,против,судья # esport: . selected features: 609 . top features: переходить,анкета,круг турнир,рафаэль,вк,случайный образ,игровой,металлург,аудитория,сша # extreme: . selected features: 1151 . top features: передать,реал,майами,рассказать,канадский,журналист,число,мадрид,мир,деталь # football: . selected features: 597 . top features: второй круг,руслан,исполнять,аарон,поражение,комментарий,регулярный,партия,финансовый,потребовать # hockey: . selected features: 877 . top features: завершиться победа,теннисист,число подбор,уокер,парень число,суперлига,скотт,билл,продать,зелёный # martial_arts: . selected features: 716 . top features: число подбор,спорт зож,спорт забег,остановка,теннис,спринт,число быть,число самый,опасный,делисьбегом # motosport: . selected features: 1019 . top features: подразделение,представить,низ,срочно,родитель,тарасенко,спартак,приблизиться,бегун,маятник # tennis: . selected features: 1033 . top features: беларусь,предложение,турнир который,мма,чёрный,кубок мир,лига чемпион,переходить,трактор,стрим # volleyball: . selected features: 914 . top features: учесть,нижний,токио,число подбор,этап чемпионат,сезон провести,иванович,никитин,манчестер,медаль # winter_sport: . selected features: 912 . top features: лига чемпион,саранск,урал,новогодний,макс,беларусь число,найти,тренировочный,хк,рассуждать
We can refit the vectorizer on the corpus by giving this new set of words as input. That will produce a smaller feature matrix and a shorter vocabulary.
vectorizer = feature_extraction.text.TfidfVectorizer(vocabulary=X_names)
vectorizer.fit(corpus)
X_train = vectorizer.transform(corpus)
dic_vocabulary = vectorizer.vocabulary_
classifier = naive_bayes.MultinomialNB()
## pipeline
model = pipeline.Pipeline([("vectorizer", vectorizer),
("classifier", classifier)])
## train classifier
model["classifier"].fit(X_train, y_train)
## test
X_test = df_test["text_clean"].values
y_pred = model.predict(X_test)
predicted_prob = model.predict_proba(X_test)
from text_classifier.common.evaluation import run_classification_report
run_classification_report(y_test, y_pred, predicted_prob, CLASSES)
Accuracy: 0.77
Auc: 0.97
Detail:
precision recall f1-score support
athletics 0.79 0.81 0.80 515
autosport 0.71 0.80 0.75 602
basketball 0.94 0.75 0.84 493
boardgames 0.79 0.92 0.85 477
esport 0.73 0.57 0.64 623
extreme 0.56 0.65 0.60 590
football 0.76 0.68 0.72 522
hockey 0.85 0.82 0.83 624
martial_arts 0.74 0.71 0.72 518
motosport 0.85 0.74 0.79 540
tennis 0.89 0.94 0.91 503
volleyball 0.73 0.82 0.77 506
winter_sport 0.81 0.87 0.83 602
accuracy 0.77 7115
macro avg 0.78 0.78 0.77 7115
weighted avg 0.78 0.77 0.77 7115
from lime import lime_text
def run_lime_explanation(df_test, y_test, y_pred, probas, i, classes, model):
txt_instance = df_test["text_clean"].iloc[i]
# check true value and predicted value
print("True:", y_test[i], "--> Predicted:", y_pred[i], "| Prob:", round(np.max(probas[i]), 2))
# show explanation
explainer = lime_text.LimeTextExplainer(class_names=model.classes_)
explained = explainer.explain_instance(txt_instance, model.predict_proba, num_features=5, top_labels=len(classes))
explained.show_in_notebook(text=txt_instance, predict_proba=False)
i = 100
run_lime_explanation(df_test, y_test, y_pred, predicted_prob, i, CLASSES, model)
True: autosport --> Predicted: autosport | Prob: 0.96
i = 200
run_lime_explanation(df_test, y_test, y_pred, predicted_prob, i, y_train, model)
True: winter_sport --> Predicted: winter_sport | Prob: 0.13
i = 3
run_lime_explanation(df_test, y_test, y_pred, predicted_prob, i, y_train, model)
True: tennis --> Predicted: tennis | Prob: 0.68
i = 4
run_lime_explanation(df_test, y_test, y_pred, predicted_prob, i, y_train, model)
True: boardgames --> Predicted: boardgames | Prob: 0.99
i = 5
run_lime_explanation(df_test, y_test, y_pred, predicted_prob, i, y_train, model)
True: boardgames --> Predicted: boardgames | Prob: 0.27
i = 6
run_lime_explanation(df_test, y_test, y_pred, predicted_prob, i, y_train, model)
True: autosport --> Predicted: autosport | Prob: 0.74
i = 7
run_lime_explanation(df_test, y_test, y_pred, predicted_prob, i, y_train, model)
True: motosport --> Predicted: autosport | Prob: 0.53
i = 13
run_lime_explanation(df_test, y_test, y_pred, predicted_prob, i, y_train, model)
True: martial_arts --> Predicted: extreme | Prob: 0.3
# df_test = df_test.reset_index()
df_test["predicted_prob"] = predicted_prob.max(axis=1)
df_test["predicted"] = y_pred
wrong_pred_df = df_test[y_test != y_pred]
wrong_pred_df.category.value_counts()
category esport 266 extreme 208 football 165 martial_arts 150 motosport 140 basketball 123 autosport 119 hockey 111 athletics 96 volleyball 92 winter_sport 81 boardgames 38 tennis 32 Name: count, dtype: int64
wrong_pred_df.predicted_prob.hist(bins=20);
wrong_pred_df[wrong_pred_df.predicted_prob < 0.5][1:10].text.values
array(['Обувная ложка меч Kessak подарок для настоящего мужчины 33 Узнать цену и сделать заказ ВКонтакте tokentokenoid WhatsApp tokentokenoid Выглядит стильно дорого и приятна на ощупь 33 Станет роскошным украшением для интерьера 33 Вы точно удивите всех оригинальным подарком 33 Не гнется Гарантия на изделие 5 лет Оплата после получения Отправка в течение 24 часов быстрая доставка Отбалансирована как настоящий самурайский меч Оптимальная длина чтобы не наклоняться Индивидуальная гравировка Единственный производитель в России Ручная работа Подвес держатель в подарок Наши обувные ложки вызывают настоящие эмоции даже у самых брутальных мужчин 33 Дарите лучшие подарки своим близким 33 Пишите и получите скидку 33 ВКонтакте tokentokenoid WhatsApp tokentokenoid',
'Друзья до 10 апреля проходил сбор заявок на конкурс Лучший болельщик МХЛ. Мы получили много откликов. Всем спасибо 33 От нашего клуба на конкурс отправляется это видео с мегапозитивными Юлией и Стасом вы их конечно знаете 33 Победителей жюри МХЛ определит совсем скоро и пригласит на церемонию закрытия сезона',
'Жидкий азот позволил разогнать Intel Core tokenoid 13900K до 8 2 ГГц Центральные процессоры Intel семейства Raptor Lake начнут продаваться 20 октября. Компания Intel провела мероприятие предшествующее началу продаж этих процессоров и приглашенный на него энтузиаст с псевдонимом Sptokenoid смог разогнать флагманскую модель Core tokenoid 13900K до 8 2 ГГц используя для охлаждения резервуар с жидким азотом. Напомним что Core tokenoid 13900K является 24 ядерным процессором с 32 потоками и максимальной частотой автоматического разгона в 5 8 ГГц. Чип обладает 16 энергоэффективными E ядрами и восемью мощными P ядрами. Одно из последних как раз и получилось разогнать до 8 2 ГГц тогда как остальные работали на 5 7–6 3 ГГц. В эксперименте участвовала материнская плата ASRotokenoid Z790 Tatokenoid напряжение на процессоре было поднято до 1 824 В а жидкий азот обеспечил охлаждение до минус 193 градусов по шкале Цельсия. Указанную частоту можно считать только модельным рекордом поскольку процессоры Intel Cetokenoid D поколения Cetokenoid Mtokenoid уже довольно давно покорили частоты 8 5 ГГц в аналогичных условиях и такой результат позволил попасть в пятерку лучших по версии HWBot. А абсолютным чемпионом по частоту является AMD FX 8370 с результатом 8 72 ГГц. Так что достигнутая Core tokenoid 13900K частота 8 2 ГГц не попадает даже в первую двадцатку абсолютного мирового рейтинга. Тем более что на видео с мероприятия последней зафиксированной на снимке экрана частотой в CPU Z стала отметка 8 0 ГГц после чего система потеряла стабильность и ушла в синий экран. В сравнении с конкурирующим Ryzen 9 7950X частота Intel Core впечатляет поскольку этот процессор AMD под жидким азотом смог разогнаться только до 7472 МГц хотя и сохранял при этом активность не только шестнадцати ядер но и тридцати двух потоков. В случае с Core tokenoid 13900K многопоточность была отключена. С другой стороны Core tokenoid 12900KS который может считаться самой быстрой моделью прошлого поколения в начале прошлого месяца покорил рубеж 7600 МГц при активности 16 ядер и 24 потоков поэтому дополнительные 600 МГц частоты в любом случае демонстрируют заметный прирост потенциала. Источник Tom 39 s Hartokenoid',
'Возможно кого то эта новость разочарует но приземление прошло успешно.',
'️ Баадур Джобава сегодня сыграет с подписчиками клуба ChessCtokenoid. Присоединяетесь Баадур Джобава грузинский шахматист гроссмейстер неоднократный чемпион Грузии. Участник 7 олимпиад на XXXVI й в Кальвии занял 1 е место в личном зачете. Чемпион Европы по рапиду. Присоединяйтесь к клубу за 1 минуту там уже более 40 лекций гроб гайд и еще много полезного контента Смотреть стрим здесь Каждый сможет задать вопрос Баадуру. Начало в 17 00 по мск',
'Сейчас время сделать справедливость реальностью для всех детей Божиих Мартин Лютер Кинг',
'Страшно крутая атмосфера в Жлобине Смотрите ужасно классную галерею со вчерашнего матча – тут',
'Иногда я чувствую что для большинства тренеров молодежи важнее всего выигрывать игры. В основном они заинтересованы в собственном успехе и собственной репутации. Но мне было плевать на очки потому что я был занят развитием игрока. С Йохан Кройф',
'Вот это прыжок Хочешь тоже доставать рукой 380 см и при этом иметь крепкие суставы? Проголосуй и напиши в комментариях'],
dtype=object)
From visual analize we can see that wrong preidctions with low confedence(aka probabilty of a predicted label) often contain comments and adds in theis texts. It maybe true that such texts should be labeled as UNKNOWN as they don't really belong to any of a categories.
wrong_pred_df[wrong_pred_df.predicted_prob > 0.5]
| oid | category | text | text_clean | predicted_prob | predicted | |
|---|---|---|---|---|---|---|
| 31 | 148430451 | motosport | MotoGP Ринс принес уходящей из Больших Призов ... | ринс принести уходить больший приз победа посл... | 0.531287 | autosport |
| 87 | 993319379 | athletics | Венгры скрестили волейбол и настольный теннис.... | венгр скрестить волейбол настольный теннис сто... | 0.748373 | volleyball |
| 1391 | 602176355 | extreme | Волны приключения детство футболки бордшорты г... | волна приключение детство футболка бордшорты г... | 0.587868 | boardgames |
| 2121 | 620580751 | extreme | А вы в курсе что Геннадий Малахов является мас... | курс геннадий малахов являться мастер спорт сс... | 0.515860 | martial_arts |
| 2277 | 344615861 | boardgames | Hotokenoid Games не только в ВК 33 У нашей сет... | вк число наш сеть аккаунт многий соцсеть удобн... | 0.518087 | volleyball |
| ... | ... | ... | ... | ... | ... | ... |
| 37015 | 587374870 | esport | У Барселоны проблемы с регистрацией новичков. ... | барселона проблема регистрация новичок каталон... | 0.526192 | football |
| 37339 | 792608859 | basketball | ТОЛЬКО ОРИГИНАЛ У нас огромный выбор баскетбол... | оригинал огромный выбор баскетбольный кроссово... | 0.518762 | extreme |
| 37368 | 297653587 | football | На 81 году умер легендарный скаут ХК ЦСКА Бори... | число год умереть легендарный скаут хк цска бо... | 0.670272 | hockey |
| 37650 | 417253268 | volleyball | Винярски стал главным тренером сборной Германи... | винярски стать главный тренер сборная германия... | 0.526797 | football |
| 38695 | 301651298 | esport | Барса даст Левандовскому двухлетний или трехле... | барс дать левандовский двухлетний трёхлетний к... | 0.717956 | football |
98 rows × 6 columns
For wrong predictions with high confidence > 0.5, it is seen 3 cases:
ЛАЙФХАК ДЛЯ ЛЫЖНИКА rtokenoid лыжи лыжник лыжня трамплин спортзимой зима бег8 – 10 апреля на городской трассе Альберт Парк в Мельбурне пройдет третий этап Формулы 1 сезона 2022. Оцениваем шансы пилотов и команд.'ЭТО БОМБА 33 33 33 Доставка из Москвы 33 33 33 Настоящий релакс для Ваших Мышц 33 Заказать по скидке Это действительно эффективный массажер который помогает снять напряжение после тренировок сделать разогрев до занятия спортом убрать боль и напряжение в мышцах. ...'UNKNOWN category, otherwise return predicted categoty.